package agency.tango.materialintroscreen.widgets;

import agency.tango.materialintroscreen.listeners.MyAnimatorStateListener;
import ohos.agp.animation.Animator;
import ohos.agp.animation.AnimatorValue;
import ohos.agp.colors.RgbColor;
import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.components.PageSlider;
import ohos.agp.components.element.ShapeElement;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.render.Path;
import ohos.agp.utils.Color;
import ohos.agp.utils.Point;
import ohos.agp.utils.RectFloat;
import ohos.app.Context;

import agency.tango.materialintroscreen.AttrString;
import agency.tango.materialintroscreen.animations.AnimatorValueUtils;
import agency.tango.materialintroscreen.animations.MyValueAnimator;
import agency.tango.materialintroscreen.interceptor.FastOutSlowInInterpolator;
import agency.tango.materialintroscreen.interceptor.Interpolator;

import java.util.Arrays;

/**
 * @ProjectName: material-intro-screen-master
 * @Package: agency.tango.materialintroscreen.widgets
 * @ClassName: InkPageIndicator
 * @Description: pageslider的指示器
 * @CreateDate: 2021/4/12 18:13
 * @UpdateDate: 2021/4/12 18:13
 * @UpdateRemark: 暂无
 * @Version: 1.0
 */
public class InkPageIndicator extends Component implements PageSlider.PageChangedListener,
        Component.BindStateChangedListener, Component.EstimateSizeListener,
        Component.DrawTask {
    private static final int DEFAULT_DOT_SIZE = 8;
    private static final int DEFAULT_GAP = 12;
    private static final int DEFAULT_ANIM_DURATION = 400;
    private static final int DEFAULT_UNSELECTED_COLOUR = 0x80ffffff;
    private static final int DEFAULT_SELECTED_COLOUR = 0xffffffff;
    private static final float INVALID_FRACTION = -1f;
    private static final float MINIMAL_REVEAL = 0.00001f;

    private float endX1;
    private float endY1;
    private float endX2;
    private float endY2;
    private float controlX1;
    private float controlY1;
    private float controlX2;
    private float controlY2;
    private int dotDiameter; // 直径
    private int gap; // 间隙
    private long animDuration; // 动画时间
    private int unselectedColour;
    private float dotRadius;
    private float halfDotRadius;
    private long animHalfDuration;
    private float dotTopY;
    private float dotCenterY;
    private float dotBottomY;
    private int pageCount;
    private int currentPage;
    private int previousPage;
    private float selectedDotX;
    private boolean selectedDotInPosition;
    private float[] dotCenterX;
    private float[] joiningFractions;
    private float retreatingJoinX1; // 后退X
    private float retreatingJoinX2;
    private float[] dotRevealFractions;
    private boolean isAttachedToWindow;
    private boolean pageChanging;
    private Paint selectedPaint;
    private Path unselectedDotPath;
    private Path unselectedDotLeftPath;
    private Path unselectedDotRightPath;
    private RectFloat rectF;
    private Interpolator interpolator;
    private Paint unselectedPaint;
    private Path combinedUnselectedPath;
    private AnimatorValue moveAnimation;
    private PendingRetreatAnimator retreatAnimation;
    private PendingRevealAnimator[] revealAnimations;
    private SwipeableViewPager viewPager;

    public InkPageIndicator(Context context, AttrSet attrs) {
        this(context, attrs, 0);
    }

    /**
     * 构造器
     *
     * @param context  上下文
     * @param attrs    属性集
     * @param defStyle 资源id
     */
    public InkPageIndicator(Context context, AttrSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        final int density =
                context.getResourceManager().getDeviceCapability().screenDensity / 160;
        dotDiameter =
                attrs.getAttr(AttrString.INKPAGEINDICATOR_DOTDIAMETER).isPresent() ?
                        attrs.getAttr(AttrString.INKPAGEINDICATOR_DOTDIAMETER).get().getIntegerValue() :
                        DEFAULT_DOT_SIZE * density;
        gap = attrs.getAttr(AttrString.INKPAGEINDICATOR_DOTGAP).isPresent() ?
                attrs.getAttr(AttrString.INKPAGEINDICATOR_DOTGAP).get().getIntegerValue() :
                DEFAULT_GAP * density;

        dotRadius = dotDiameter / 2;
        halfDotRadius = dotRadius / 2;
        animDuration =
                attrs.getAttr(AttrString.INKPAGEINDICATOR_ANIMATIONDURATION).isPresent() ?
                        attrs.getAttr(AttrString.INKPAGEINDICATOR_ANIMATIONDURATION).get().getIntegerValue() :
                        DEFAULT_ANIM_DURATION;
        animHalfDuration = animDuration / 2;
        unselectedColour = getColorFromAttr(attrs,
                AttrString.INKPAGEINDICATOR_PAGEINDICATORCOLOR,
                DEFAULT_UNSELECTED_COLOUR);
        int selectedColour = getColorFromAttr(attrs,
                AttrString.INKPAGEINDICATOR_CURRENTPAGEINDICATORCOLOR,
                DEFAULT_SELECTED_COLOUR);
        ShapeElement element = new ShapeElement();
        RgbColor rgbColor = new RgbColor(0x00000000);
        element.setRgbColor(rgbColor);
        setBackground(element);
        unselectedPaint = new Paint();
        unselectedPaint.setAntiAlias(true);
        unselectedPaint.setColor(new Color(unselectedColour));
        selectedPaint = new Paint();
        selectedPaint.setAntiAlias(true);
        selectedPaint.setColor(new Color(selectedColour));
        interpolator = new FastOutSlowInInterpolator();

        combinedUnselectedPath = new Path();
        unselectedDotPath = new Path();
        unselectedDotLeftPath = new Path();
        unselectedDotRightPath = new Path();
        rectF = new RectFloat();
        setBindStateChangedListener(this);
        setEstimateSizeListener(this);
        addDrawTask(this);
    }

    /**
     * 获取自定义属性值
     *
     * @param attrs        属性集
     * @param name         自定义属性名
     * @param defaultValue 默认值
     * @return 自定义属性值
     */
    public static int getColorFromAttr(AttrSet attrs, String name, int defaultValue) {
        int value = defaultValue;
        if (attrs.getAttr(name) != null && attrs.getAttr(name).isPresent()) {
            value = attrs.getAttr(name).get().getColorValue().getValue();
        }
        return value;
    }

    /**
     * 获取pageslider的子条目个数
     *
     * @return 子条目个数
     */
    private int getCount() {
        return viewPager.getAdapter().getCount();
    }

    /**
     * 设置pageslider
     *
     * @param viewPager 需要设置的pageslider
     */
    public void setViewPager(final SwipeableViewPager viewPager) {
        this.viewPager = viewPager;
        viewPager.addOnPageChangeListeners(this);
        setPageCount(getCount());
        viewPager.getAdapter().registerDataSetObserver(() -> {
            setPageCount(getCount());
        });
        setCurrentPageImmediate();
    }

    @Override
    public void onPageSliding(int position, float positionOffset, int positionOffsetPixels) {
        if (isAttachedToWindow) {
            float fraction = positionOffset;
            int currentPosition = pageChanging ? previousPage : currentPage;
            int leftDotPosition = position;

            if (currentPosition != position) {
                fraction = 1f - positionOffset;

                if (fraction == 1f) {
                    leftDotPosition = Math.min(currentPosition, position);
                }
            }
            setJoiningFraction(leftDotPosition, fraction);
        }
    }

    @Override
    public void onPageSlideStateChanged(int position) {
        // do nothing
    }

    @Override
    public void onPageChosen(int position) {
        if (position < pageCount) {
            if (isAttachedToWindow) {
                setSelectedPage(position);
            } else {
                setCurrentPageImmediate();
            }
        }
    }

    /**
     * pageslider数据更新  同步更新指示器的状态
     *
     * @param pages pageslider子条目数
     */
    private void setPageCount(int pages) {
        if (pages > 0) {
            pageCount = pages;
            resetState();
            getContext().getMainTaskDispatcher().asyncDispatch(this::invalidate);
        }
    }

    /**
     * 计算指示器小圆点的位置
     *
     * @param width 指示器宽度
     */
    private void calculateDotPositions(int width) {
        int left = getPaddingLeft();
        int top = getPaddingTop();
        int right = width - getPaddingRight();

        int requiredWidth = getRequiredWidth();
        float startLeft =
                left + ((right - left - requiredWidth) / 2) + dotRadius;

        dotCenterX = new float[pageCount];
        for (int i = 0; i < pageCount; i++) {
            dotCenterX[i] = startLeft + i * (dotDiameter + gap);
        }
        dotTopY = top;
        dotCenterY = top + dotRadius;
        dotBottomY = top + dotDiameter;

        setCurrentPageImmediate();
    }

    /**
     * 根据pageslider的当前条目同步设置当前选中指示器
     */
    private void setCurrentPageImmediate() {
        if (viewPager != null) {
            currentPage = viewPager.getCurrentPage();
        } else {
            currentPage = 0;
        }
        if (currentPage < 0) {
            currentPage = 0;
        }
        if (isDotAnimationStarted()) {
            selectedDotX = dotCenterX[currentPage];
        }
    }

    /**
     * 判断指示器动画是否开始
     *
     * @return 是否开始
     */
    private boolean isDotAnimationStarted() {
        return dotCenterX != null && dotCenterX.length > 0 && (moveAnimation == null || !moveAnimation.isRunning());
    }

    /**
     * 重置指示器状态
     */
    private void resetState() {
        joiningFractions = new float[pageCount - 1];
        Arrays.fill(joiningFractions, 0f);
        dotRevealFractions = new float[pageCount];
        Arrays.fill(dotRevealFractions, 0f);
        retreatingJoinX1 = INVALID_FRACTION;
        retreatingJoinX2 = INVALID_FRACTION;
        selectedDotInPosition = true;
    }


    @Override
    public boolean onEstimateSize(int widthMeasureSpec, int heightMeasureSpec) {
        int desiredHeight = getDesiredHeight();
        int height;
        switch (EstimateSpec.getMode(heightMeasureSpec)) {
            case EstimateSpec.PRECISE:
                height = EstimateSpec.getSize(heightMeasureSpec);
                break;
            case EstimateSpec.NOT_EXCEED:
                height = Math.min(desiredHeight,
                        EstimateSpec.getSize(heightMeasureSpec));
                break;
            default:
                height = desiredHeight;
                break;
        }

        int desiredWidth = getDesiredWidth();
        int width;
        switch (EstimateSpec.getMode(widthMeasureSpec)) {
            case EstimateSpec.PRECISE:
                width = EstimateSpec.getSize(widthMeasureSpec);
                break;
            case EstimateSpec.NOT_EXCEED:
                width = Math.min(desiredWidth,
                        EstimateSpec.getSize(widthMeasureSpec));
                break;
            default:
                width = desiredWidth;
                break;
        }
        setEstimatedSize(EstimateSpec.getSizeWithMode(width,
                EstimateSpec.PRECISE),
                EstimateSpec.getSizeWithMode(height, EstimateSpec.PRECISE));
        calculateDotPositions(width);
        return true;
    }

    /**
     * 获取指示器所需高度
     *
     * @return 指示器所需高度
     */
    private int getDesiredHeight() {
        return getPaddingTop() + dotDiameter + getPaddingBottom();
    }

    /**
     * 获取指示器所需最少宽度
     *
     * @return 指示器所需最少宽度
     */
    private int getRequiredWidth() {
        return pageCount * dotDiameter + (pageCount - 1) * gap;
    }

    /**
     * 获取指示器所需总宽度
     *
     * @return 指示器所需总宽度
     */
    private int getDesiredWidth() {
        return getPaddingLeft() + getRequiredWidth() + getPaddingRight();
    }

    @Override
    public void onComponentBoundToWindow(Component component) {
        isAttachedToWindow = true;
    }

    @Override
    public void onComponentUnboundFromWindow(Component component) {
        isAttachedToWindow = false;
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        if (viewPager == null || pageCount == 0) {
            return;
        }
        drawUnselected(canvas);
        drawSelected(canvas);
    }

    /**
     * 绘制未选中小圆点
     *
     * @param canvas 画布
     */
    private void drawUnselected(Canvas canvas) {
        combinedUnselectedPath.rewind();
        for (int page = 0; page < pageCount; page++) {
            int nextXIndex;

            if (page == pageCount - 1) {
                nextXIndex = page;
            } else {
                nextXIndex = page + 1;
            }

            Path unselectedPath = getUnselectedPath(page,
                    dotCenterX[page],
                    dotCenterX[nextXIndex],
                    page == pageCount - 1 ? INVALID_FRACTION :
                            joiningFractions[page],
                    dotRevealFractions[page]);

            unselectedPath.addPath(combinedUnselectedPath);
            combinedUnselectedPath.addPath(unselectedPath);
        }

        if (retreatingJoinX1 != INVALID_FRACTION) {
            Path retreatingJoinPath = getRetreatingJoinPath();
            combinedUnselectedPath.addPath(retreatingJoinPath);
        }

        canvas.drawPath(combinedUnselectedPath, unselectedPaint);
    }

    /**
     * 绘制未选中小圆点路径
     *
     * @param page              当前页
     * @param centerX           当前页中心点x坐标
     * @param nextCenterX       下一页中心点x坐标
     * @param joiningFraction   所有小圆点当前偏移量
     * @param dotRevealFraction 选中小圆点当前偏移量
     * @return 未选中小圆点路径
     */
    private Path getUnselectedPath(int page, float centerX, float nextCenterX,
                                   float joiningFraction, float dotRevealFraction) {
        unselectedDotPath.rewind();
        if (isDotNotJoining(page, joiningFraction, dotRevealFraction)) {
            unselectedDotPath.addCircle(dotCenterX[page], dotCenterY,
                    dotRadius, Path.Direction.CLOCK_WISE);
        }

        if (isDotJoining(joiningFraction)) {
            addLeftPath(centerX, joiningFraction);
            addRightPath(nextCenterX, joiningFraction);
        }

        if (joiningFraction > 0.5f && joiningFraction < 1f && retreatingJoinX1 == INVALID_FRACTION) {
            drawPathMoreThanHalf(centerX, nextCenterX, joiningFraction);
        }
        if (joiningFraction == 1 && retreatingJoinX1 == INVALID_FRACTION) {
            rectF.modify(centerX - dotRadius, dotTopY,
                    nextCenterX + dotRadius, dotBottomY);
            unselectedDotPath.addRoundRect(rectF, dotRadius, dotRadius,
                    Path.Direction.CLOCK_WISE);
        }
        if (dotRevealFraction > MINIMAL_REVEAL) {
            unselectedDotPath.addCircle(centerX, dotCenterY,
                    dotRevealFraction * dotRadius,
                    Path.Direction.CLOCK_WISE);
        }
        return unselectedDotPath;
    }

    private void drawPathMoreThanHalf(float centerX, float nextCenterX, float joiningFraction) {
        float adjustedFraction = (joiningFraction - 0.2f) * 1.25f;
        unselectedDotPath.moveTo(centerX, dotBottomY);
        rectF.modify(centerX - dotRadius, dotTopY, centerX + dotRadius,
                dotBottomY);
        unselectedDotPath.arcTo(rectF, 90, 180, true);

        endX1 = centerX + dotRadius + (gap / 2);
        endY1 = dotCenterY - (adjustedFraction * dotRadius);
        controlX1 = endX1 - (adjustedFraction * dotRadius);
        controlY1 = dotTopY;
        controlX2 = endX1 - ((1 - adjustedFraction) * dotRadius);
        controlY2 = endY1;
        unselectedDotPath.cubicTo(new Point(controlX1, controlY1),
                new Point(controlX2, controlY2),
                new Point(endX1, endY1));

        endX2 = nextCenterX;
        endY2 = dotTopY;
        controlX1 = endX1 + ((1 - adjustedFraction) * dotRadius);
        controlY1 = endY1;
        controlX2 = endX1 + (adjustedFraction * dotRadius);
        controlY2 = dotTopY;
        unselectedDotPath.cubicTo(new Point(controlX1, controlY1),
                new Point(controlX2, controlY2),
                new Point(endX2, endY2));

        rectF.modify(nextCenterX - dotRadius, dotTopY,
                nextCenterX + dotRadius, dotBottomY);
        unselectedDotPath.arcTo(rectF, 270, 180, true);

        endY1 = dotCenterY + (adjustedFraction * dotRadius);
        controlX1 = endX1 + (adjustedFraction * dotRadius);
        controlY1 = dotBottomY;
        controlX2 = endX1 + ((1 - adjustedFraction) * dotRadius);
        controlY2 = endY1;
        unselectedDotPath.cubicTo(new Point(controlX1, controlY1),
                new Point(controlX2, controlY2),
                new Point(endX1, endY1));

        endX2 = centerX;
        endY2 = dotBottomY;
        controlX1 = endX1 - ((1 - adjustedFraction) * dotRadius);
        controlY1 = endY1;
        controlX2 = endX1 - (adjustedFraction * dotRadius);
        controlY2 = endY2;
        unselectedDotPath.cubicTo(new Point(controlX1, controlY1),
                new Point(controlX2, controlY2),
                new Point(endX2, endY2));
    }

    private void addRightPath(float nextCenterX, float joiningFraction) {
        unselectedDotRightPath.rewind();
        unselectedDotRightPath.moveTo(nextCenterX, dotBottomY);
        rectF.modify(nextCenterX - dotRadius, dotTopY,
                nextCenterX + dotRadius, dotBottomY);
        unselectedDotRightPath.arcTo(rectF, 90, -180, true);

        endX1 = nextCenterX - dotRadius - (joiningFraction * gap);
        endY1 = dotCenterY;
        controlX1 = nextCenterX - halfDotRadius;
        controlY1 = dotTopY;
        controlX2 = endX1;
        controlY2 = endY1 - halfDotRadius;
        unselectedDotRightPath.cubicTo(new Point(controlX1, controlY1),
                new Point(controlX2, controlY2),
                new Point(endX1, endY1));

        endX2 = nextCenterX;
        endY2 = dotBottomY;
        controlX1 = endX1;
        controlY1 = endY1 + halfDotRadius;
        controlX2 = endX2 - halfDotRadius;
        controlY2 = dotBottomY;
        unselectedDotRightPath.cubicTo(new Point(controlX1, controlY1),
                new Point(controlX2, controlY2),
                new Point(endX2, endY2));
        unselectedDotPath.addPath(unselectedDotRightPath);
    }

    private void addLeftPath(float centerX, float joiningFraction) {
        unselectedDotLeftPath.rewind();
        unselectedDotLeftPath.moveTo(centerX, dotBottomY);
        rectF.modify(centerX - dotRadius, dotTopY, centerX + dotRadius,
                dotBottomY);
        unselectedDotLeftPath.arcTo(rectF, 90, 180, true);

        endX1 = centerX + dotRadius + (joiningFraction * gap);
        endY1 = dotCenterY;
        controlX1 = centerX + halfDotRadius;
        controlY1 = dotTopY;
        controlX2 = endX1;
        controlY2 = endY1 - halfDotRadius;
        unselectedDotLeftPath.cubicTo(new Point(controlX1, controlY1),
                new Point(controlX2, controlY2),
                new Point(endX1, endY1));

        endX2 = centerX;
        endY2 = dotBottomY;
        controlX1 = endX1;
        controlY1 = endY1 + halfDotRadius;
        controlX2 = centerX + halfDotRadius;
        controlY2 = dotBottomY;
        unselectedDotLeftPath.cubicTo(new Point(controlX1, controlY1),
                new Point(controlX2, controlY2),
                new Point(endX2, endY2));

        unselectedDotPath.addPath(unselectedDotLeftPath);
    }

    /**
     * 判断两个变形的指示器小圆点是否相交
     *
     * @param joiningFraction 所有小圆点当前偏移量
     * @return 是否相交
     */
    private boolean isDotJoining(float joiningFraction) {
        return joiningFraction > 0f && joiningFraction <= 0.5f && retreatingJoinX1 == INVALID_FRACTION;
    }

    /**
     * 两个变形的指示器小圆点是否不相交
     *
     * @param page              当前选中页面
     * @param joiningFraction   所有小圆点当前偏移量
     * @param dotRevealFraction 选中小圆点当前偏移量
     * @return 两个变形的指示器小圆点是否不相交
     */
    private boolean isDotNotJoining(int page, float joiningFraction,
                                    float dotRevealFraction) {
        return (joiningFraction == 0f || joiningFraction == INVALID_FRACTION)
                && dotRevealFraction == 0f
                && !(page == currentPage && selectedDotInPosition);
    }

    private Path getRetreatingJoinPath() {
        unselectedDotPath.rewind();
        rectF.modify(retreatingJoinX1, dotTopY, retreatingJoinX2, dotBottomY);
        unselectedDotPath.addRoundRect(rectF, dotRadius, dotRadius,
                Path.Direction.CLOCK_WISE);
        return unselectedDotPath;
    }

    private void drawSelected(Canvas canvas) {
        canvas.drawCircle(selectedDotX, dotCenterY, dotRadius, selectedPaint);
    }

    private void setSelectedPage(int now) {
        if (now == currentPage) {
            return;
        }

        pageChanging = true;
        previousPage = currentPage;
        currentPage = now;
        if (currentPage < 0) {
            currentPage = 0;
        }
        final int steps = Math.abs(now - previousPage);

        if (steps > 1) {
            if (now > previousPage) {
                for (int i = 0; i < steps; i++) {
                    setJoiningFraction(previousPage + i, 1f);
                }
            } else {
                for (int i = -1; i > -steps; i--) {
                    setJoiningFraction(previousPage + i, 1f);
                }
            }
        }

        moveAnimation = createMoveSelectedAnimator(dotCenterX[now],
                previousPage, now, steps);
        moveAnimation.start();
    }

    private AnimatorValue createMoveSelectedAnimator(
            final float moveTo, int was, int now, int steps) {
        MyValueAnimator moveSelected = MyValueAnimator.ofFloat(selectedDotX,
                moveTo);
        retreatAnimation = new PendingRetreatAnimator(was, now, steps,
                now > was ?
                        new RightwardStartPredicate(moveTo - ((moveTo - selectedDotX) * 0.25f)) :
                        new LeftwardStartPredicate(moveTo + ((selectedDotX - moveTo) * 0.25f)));
        addListener(moveSelected);
        moveSelected.setDelay(selectedDotInPosition ? animDuration / 4L : 0L);
        moveSelected.setDuration(animDuration * 3L / 4L);
        moveSelected.setCurve(interpolator);
        return moveSelected;
    }

    private void addListener(MyValueAnimator moveSelected) {
        retreatAnimation.setStateChangedListener(new MyAnimatorStateListener() {
            @Override
            public void onEnd(Animator animator) {
                resetState();
                pageChanging = false;
            }
        });
        moveSelected.setValueUpdateListener((animatorValue, value) -> {
            selectedDotX = value;
            retreatAnimation.startIfNecessary(selectedDotX);
            getContext().getUITaskDispatcher().asyncDispatch(this::invalidate);
        });

        moveSelected.setStateChangedListener(new MyAnimatorStateListener() {
            @Override
            public void onStart(Animator animator) {
                selectedDotInPosition = false;
            }
        });
    }

    private void setJoiningFraction(int leftDot, float fraction) {
        if (joiningFractions != null) {
            if (leftDot < joiningFractions.length) {
                joiningFractions[leftDot] = fraction;
                invalidate();
            }
        }
    }

    /**
     * 清除圆点交汇数值
     */
    public void clearJoiningFractions() {
        Arrays.fill(joiningFractions, 0f);
        invalidate();
    }

    private void setDotRevealFraction(int dot, float fraction) {
        if (dot < dotRevealFractions.length) {
            dotRevealFractions[dot] = fraction;
        }
        invalidate();
    }

    /**
     * 设置指示器的颜色
     *
     * @param secondaryColor 指示器的颜色
     */
    public void setPageIndicatorColor(int secondaryColor) {
        unselectedColour = secondaryColor;
        unselectedPaint = new Paint();
        unselectedPaint.setAntiAlias(true);
        unselectedPaint.setColor(new Color(unselectedColour));
    }


    /**
     * 圆点动画
     */
    public abstract class PendingStartAnimator extends MyValueAnimator {
        boolean hasStarted;
        StartPredicate predicate;


        PendingStartAnimator(StartPredicate predicate) {
            super();
            this.predicate = predicate;
            hasStarted = false;
        }

        void startIfNecessary(float currentValue) {
            if (!hasStarted && predicate.shouldStart(currentValue)) {
                start();
                hasStarted = true;
            }
        }
    }

    /**
     * 小圆点回缩动画
     */
    public class PendingRetreatAnimator extends PendingStartAnimator {
        PendingRetreatAnimator(int was, int now, int steps, StartPredicate predicate) {
            super(predicate);
            setDuration(animHalfDuration);
            setCurve(interpolator);

            // work out the start/end values of the retreating join from the
            // direction we're
            // travelling in.  Also look at the current selected dot
            // position, i.e. we're moving on
            // before a prior anim has finished.
            final float initialX1 = now > was ? Math.min(dotCenterX[was],
                    selectedDotX) - dotRadius
                    : dotCenterX[now] - dotRadius;
            final float finalX1 = now > was ? dotCenterX[now] - dotRadius
                    : dotCenterX[now] - dotRadius;
            final float initialX2 = now > was ? dotCenterX[now] + dotRadius
                    : Math.max(dotCenterX[was], selectedDotX) + dotRadius;
            final float finalX2 = now > was ? dotCenterX[now] + dotRadius
                    : dotCenterX[now] + dotRadius;

            revealAnimations = new PendingRevealAnimator[steps];
            // hold on to the indexes of the dots that will be hidden by the
            // retreat so that
            // we can initialize their revealFraction's i.e. make sure
            // they're hidden while the
            // reveal animation runs
            final int[] dotsToHide = new int[steps];
            if (Float.compare(initialX1, finalX1) != 0) {
                setFloatValues(initialX1, finalX1);
                for (int i = 0; i < steps; i++) {
                    revealAnimations[i] = new PendingRevealAnimator(was + i,
                            new RightwardStartPredicate(dotCenterX[was + i]));
                    dotsToHide[i] = was + i;
                }
                setValueUpdateListener((animatorValue, value) -> {
                    retreatingJoinX1 = value;
                    getContext().getUITaskDispatcher().asyncDispatch(InkPageIndicator.this::invalidate);
                    for (PendingRevealAnimator pendingReveal :
                            revealAnimations) {
                        pendingReveal.startIfNecessary(retreatingJoinX1);
                    }
                });
            } else {
                setFloatValues(initialX2, finalX2);
                for (int i = 0; i < steps; i++) {
                    revealAnimations[i] = new PendingRevealAnimator(was - i,
                            new LeftwardStartPredicate(dotCenterX[was - i]));
                    dotsToHide[i] = was - i;
                }
                setValueUpdateListener((animatorValue, value) -> {
                    retreatingJoinX2 = value;
                    getContext().getUITaskDispatcher().asyncDispatch(InkPageIndicator.this::invalidate);

                    for (PendingRevealAnimator pendingReveal :
                            revealAnimations) {
                        pendingReveal.startIfNecessary(retreatingJoinX2);
                    }
                });
            }
            initListener(dotsToHide, initialX1, initialX2);
        }

        private void initListener(int[] dotsToHide, final float initialX1, final float initialX2) {
            setStateChangedListener(new MyAnimatorStateListener() {
                @Override
                public void onStart(Animator animator) {
                    clearJoiningFractions();
                    for (int dot : dotsToHide) {
                        setDotRevealFraction(dot, MINIMAL_REVEAL);
                    }
                    retreatingJoinX1 = initialX1;
                    retreatingJoinX2 = initialX2;
                    getContext().getMainTaskDispatcher().asyncDispatch(InkPageIndicator.this::invalidate);
                }

                @Override
                public void onEnd(Animator animator) {
                    retreatingJoinX1 = INVALID_FRACTION;
                    retreatingJoinX2 = INVALID_FRACTION;
                    getContext().getMainTaskDispatcher().asyncDispatch(InkPageIndicator.this::invalidate);
                }
            });
        }
    }

    /**
     * 选中圆点动画
     */
    public class PendingRevealAnimator extends PendingStartAnimator {
        private int dot;

        PendingRevealAnimator(int dot, StartPredicate predicate) {
            super(predicate);
            this.dot = dot;
            setDuration(animHalfDuration);
            setCurve(interpolator);
            setValueUpdateListener((animatorValue, value) -> setDotRevealFraction(PendingRevealAnimator.this.dot,
                    AnimatorValueUtils.getAnimatedValue(value, new float[]{MINIMAL_REVEAL, 1f})));
            setStateChangedListener(new MyAnimatorStateListener() {
                @Override
                public void onEnd(Animator animator) {
                    setDotRevealFraction(PendingRevealAnimator.this.dot, 0f);
                    getContext().getMainTaskDispatcher().asyncDispatch(InkPageIndicator.this::invalidate);
                }
            });
        }
    }

    /**
     * 圆点动画
     */
    public abstract class StartPredicate {
        float thresholdValue;

        StartPredicate(float thresholdValue) {
            this.thresholdValue = thresholdValue;
        }

        abstract boolean shouldStart(float currentValue);
    }

    /**
     * 右边圆点动画
     */
    public class RightwardStartPredicate extends StartPredicate {
        RightwardStartPredicate(float thresholdValue) {
            super(thresholdValue);
        }

        boolean shouldStart(float currentValue) {
            return currentValue > thresholdValue;
        }
    }

    /**
     * 左边圆点动画
     */
    public class LeftwardStartPredicate extends StartPredicate {
        LeftwardStartPredicate(float thresholdValue) {
            super(thresholdValue);
        }

        boolean shouldStart(float currentValue) {
            return currentValue < thresholdValue;
        }
    }
}